package com.fm.parser.xml;

import com.fm.parser.xml.error.XMLParserException;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class XMLParser {

	public static void parse(InputStream in, XMLHandler handler) throws IOException, XMLParserException {
		parse(in, handler, "utf-8");
	}

	public static void parse(InputStream in, XMLHandler handler, String charset)
			throws IOException, XMLParserException {
		if (handler == null) {
			throw new NullPointerException("useless handler");
		}

		int b;

		boolean inTag = false;
		boolean inString = false;

		List<Byte> buffer = new LinkedList<>();
		while ((b = in.read()) != -1) {
			if (inTag) {
				if (b == '?') { // useless tag
					// read the tag name
					for (; ; ) {
						b = in.read();
						if (b == ' ' || b == '\r' || b == '\n' || b == '\t' || b == '>') {
							break;
						}
					}

					// read the attributes info in the tag
					for (; ; ) {
						b = in.read();
						if (b == '"' && !inString) {
							inString = true;
						} else if (b == '"') {
							inString = false;
						} else if (inString) {
							// do nothing
						} else if (b == '>') {
							inTag = false;
							break;
						}
					}
				} else if (b == '!') { // useless tag
					int temp = in.read();
					if (temp == '-') {
						if (in.read() != '-') {
							throw new XMLParserException("we find <!- and hope <!-- but actually not");
						}
						for (; ; ) {
							do {
								b = in.read();
							} while (b != '-');
							if ('-' == in.read() && '>' == in.read()) {
								inTag = false;
								break;
							}
						}
					} else {
						for (; ; ) {
							b = in.read();
							if (b == '"' && !inString) {
								inString = true;
							} else if (b == '"') {
								inString = false;
							} else if (inString) {
								// do nothing
							} else if (b == '>') {
								inTag = false;
								break;
							}
						}
					}
				} else if (b == '/') { // useful end tag
					// read the tag name
					for (; ; ) {
						b = in.read();
						if (b != ' ' && b != '\r' && b != '\n' && b != '\t' && b != '>') {
							buffer.add((byte) b);
						} else {
							break;
						}
					}

					// read to the end of tag
					while (b != '>') {
						b = in.read();
					}
					inTag = false;
					handler.doEndTag(new String(toArray(buffer), charset));
					buffer.clear();
				} else { // useful begin tag
					// read the tag name
					buffer.add((byte) b);
					for (; ; ) {
						b = in.read();
						if (b != ' ' && b != '\r' && b != '\n' && b != '\t' && b != '/' && b != '>') {
							buffer.add((byte) b);
						} else {
							break;
						}
					}
					String name = new String(toArray(buffer), charset);
					buffer.clear();
					Map<String, String> attributes = new LinkedHashMap<>();

					if (b == '>') {
						inTag = false;
						handler.doStartTag(name, attributes);
					} else {
						String attrName = null;
						String attrVal;
						for (; ; ) { // loop to read key-value
							b = in.read();
							if (b == '"' && !inString) {
								inString = true;
							} else if (b == '"') {
								inString = false;
								attrVal = new String(toArray(buffer), charset);
								buffer.clear();
								attributes.put(attrName, attrVal);
							} else if (inString) {
								buffer.add((byte) b);
							} else if (b != ' ' && b != '\r' && b != '\n' && b != '\t' && b != '=' && b != '/' && b != '>') {
								buffer.add((byte) b);
							} else if (b == '=') {
								attrName = new String(toArray(buffer), charset);
								buffer.clear();
							} else if (b == '/') {
								if (in.read() == '>') {
									inTag = false;
									handler.doStartTag(name, attributes);
									handler.doEndTag(name);
									break;
								} else {
									throw new XMLParserException("we find <" + name + " ... / and hope /> but actually not");
								}
							} else if (b == '>') {
								inTag = false;
								handler.doStartTag(name, attributes);
								break;
							}
						}
					}
				}
			} else {
				if (b == '<') { // 一个标签开始
					handler.characters(new String(toArray(buffer), charset));
					buffer.clear();
					inTag = true;
				} else {
					buffer.add((byte) b);
				}
			}
		}
	}

	private static byte[] toArray(List<Byte> list) {
		byte[] toReturn = new byte[list.size()];
		int index = 0;
		for (Byte b : list) {
			toReturn[index++] = b;
		}
		return toReturn;
	}
}
